package com.dbflow5.query;

import com.dbflow5.StringUtils;
import com.dbflow5.adapter.ModelAdapter;
import com.dbflow5.annotation.ConflictAction;
import com.dbflow5.config.FlowManager;
import com.dbflow5.database.DatabaseWrapper;
import com.dbflow5.query.property.IProperty;
import com.dbflow5.query.property.Property;
import com.dbflow5.sql.Query;
import com.dbflow5.structure.ChangeAction;
import ohos.data.rdb.ValuesBucket;
import ohos.utils.Pair;

import java.util.*;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Description: The SQLite INSERT command
 */
public class Insert<TModel> extends BaseQueriable<TModel> implements Query {

    /**
     * The columns to specify in this query (optional)
     */
    private List<IProperty<?>> columns = null;

    /**
     * The values to specify in this query
     */
    private List<List<Object>> valuesList = new ArrayList<>();

    /**
     * The conflict algorithm to use to resolve inserts.
     */
    private ConflictAction conflictAction = ConflictAction.NONE;

    private From<?> selectFrom = null;

    public Insert(Class<TModel> table, Property<?>... columns) {
        super(table);
        columns(columns);
    }

    @Override// append FROM, which overrides values
    public String getQuery() {
        StringBuilder q = new StringBuilder();
        q.append("INSERT ");
        if (conflictAction != null && conflictAction != ConflictAction.NONE) {
            q.append("OR "+conflictAction+" ");
        }
        q.append("INTO ");
        q.append(FlowManager.getTableName(table));
        if(columns != null){
            if (!columns.isEmpty()) {
                q.append("(");
                StringUtils.appendArray(q, columns.toArray(new Object[]{}));
                q.append(")");
            }
        }
        From<?> selectFrom = this.selectFrom;
        if (selectFrom != null) {
            q.append(" ");
            q.append(selectFrom.getQuery());
        } else {
            if (valuesList.size() < 1) {
                throw new IllegalStateException("The insert of "+FlowManager.getTableName(table)+" " +
                        "should have at least one value specified for the insert");
            } else {
                if(columns != null && !columns.isEmpty()){
                    valuesList.stream().filter(objects -> objects.size() != columns.size()).forEach(new Consumer<List<Object>>() {
                        @Override
                        public void accept(List<Object> objects) {
                            throw new IllegalStateException(
                                    "The Insert of "+ FlowManager.getTableName(table) +
                                            "|when specifying columns needs to have the same amount"+
                                            "|of values and columns. found ${it.size} != ${columns.size}");
                        }
                    });

                }
            }

            q.append(" VALUES(");
            for(int i=0;i<valuesList.size();i++){
                if (i > 0) {
                    q.append(",(");
                }
                q.append(BaseOperator.joinArguments(", ", valuesList.get(i)));
                q.append(")");
            }
        }
        return q.toString();
    }

    @Override
    public ChangeAction primaryAction() {
        return ChangeAction.INSERT;
    }

    /**
     * The optional columns to specify. If specified, the values length must correspond to these columns, and
     * each column has a 1-1 relationship to the values.
     *
     * @param columns The columns to use
     * @return this
     */
    public Insert<TModel> columns(String... columns) {
        ModelAdapter<TModel> modelClassModelAdapter = FlowManager.modelAdapter(table);
        for(String column : columns) {
            Property<?> property = modelClassModelAdapter.getProperty(column);
            this.columns.add(property);
        }
        return this;
    }

    public Insert<TModel> columns(IProperty<?>... properties) {
        this.columns = new ArrayList<>();
        for (IProperty<?> property : properties) {
            this.columns.add(property);
        }
        return this;
    }

    public Insert<TModel> columns(List<IProperty<?>> properties) {
        this.columns = properties;
        return this;
    }

    /**
     * Appends a list of columns to this INSERT statement from the associated [TModel].
     * @return Appends a list of columns to this INSERT statement from the associated [TModel].
     */
    public Insert<TModel> asColumns() {
        columns(FlowManager.modelAdapter(table).getAllColumnProperties());
        return this;
    }

    /**
     * Appends a list of columns to this INSERT and ? as the values.
     * @return Appends a list of columns to this INSERT and ? as the values.
     */
    public Insert<TModel> asColumnValues() {
        asColumns();
        if(columns != null){
            List<String> list = new ArrayList<>();
            for(IProperty<?> property : columns){
                list.add("?");
            }
            valuesList.add(Collections.singletonList(list));
        }
        return this;
    }

    /**
     * The required values to specify. It must be non-empty and match the length of the columns when
     * a set of columns are specified.
     *
     * @param values The non type-converted values
     * @return this
     */
    public Insert<TModel> values(Object... values) {
        valuesList.add(Arrays.asList(values));
        return this;
    }

    /**
     * The required values to specify. It must be non-empty and match the length of the columns when
     * a set of columns are specified.
     *
     * @param values The non type-converted values
     * @return this
     */
    public Insert<TModel> values(List<Object> values) {
        valuesList.addAll(Collections.singletonList(values));
        return this;
    }

    /**
     * Uses the [Operator] pairs to fill this insert query.
     *
     * @param conditions The conditions that we use to fill the columns and values of this INSERT
     * @return this
     */
    public Insert<TModel> columnValues(SQLOperator... conditions) {
        String[] columnNames = new String[conditions.length];
        Object[] values = new Object[conditions.length];
        int index = 0;
        for(SQLOperator sqlOperator : conditions){
            columnNames[index] = sqlOperator.columnName();
            values[index] = sqlOperator.value();
            index++;
        }
        return columns(columnNames).values(values);
    }

    /**
     * Uses the [Operator] pairs to fill this insert query.
     *
     * @param conditions The conditions that we use to fill the columns and values of this INSERT
     * @return this
     */
    public Insert<TModel> columnValues(Pair<IProperty<?>, ?>... conditions) {
        IProperty<?>[] columnNames = new IProperty<?>[conditions.length];
        Object[] values = new Object[conditions.length];
        int index = 0;
        for(Pair<IProperty<?>, ?> pair : conditions){
            columnNames[index] = pair.f;
            values[index] = pair.s;
            index++;
        }
        return columns(columnNames).values(values);
    }

    /**
     * Uses the [Operator] pairs to fill this insert query.
     *
     * @param group The [Iterable] of [SQLOperator]
     * @return this
     */
    public Insert<TModel> columnValues(Iterable<SQLOperator> group) {
        List<String> columnNames = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        for(SQLOperator sqlOperator : group){
            columnNames.add(sqlOperator.columnName());
            values.add(sqlOperator.value());
        }
        return columns(columnNames.toArray(new String[0])).values(values);
    }

    public Insert<TModel> columnValues(ValuesBucket contentValues) {
        Set<Map.Entry<String, Object>> entries = contentValues.getAll();
        int size = contentValues.getColumnSet().size();
        String[] columns = new String[size];
        Object[] values = new Object[size];
        int index = 0;
        for (Map.Entry<String, Object> entry : entries) {
            columns[index] = entry.getKey();
            values[index] = contentValues.getObject(entry.getKey());
        }

        return columns(columns).values(values);
    }

    /**
     * Appends the specified [From], which comes from a [Select] statement.
     *
     * @param selectFrom The from that is continuation of [Select].
     * @return this
     */
    public Insert<TModel> select(From<?> selectFrom) {
        this.selectFrom = selectFrom;
        return this;
    }


    /**
     * Specifies the optional OR method to use for this insert query
     *
     * @param action The conflict action to use
     * @return this
     */
    public Insert<TModel> or(ConflictAction action) {
        conflictAction = action;
        return this;
    }

    /**
     * Specifies OR REPLACE, which will either insert if row does not exist, or replace the value if it does.
     *
     * @return this
     */
    public Insert<TModel> orReplace() {
        return or(ConflictAction.REPLACE);
    }

    /**
     * Specifies OR ROLLBACK, which will cancel the current transaction or ABORT the current statement.
     *
     * @return this
     */
    public Insert<TModel> orRollback() {
        return or(ConflictAction.ROLLBACK);
    }

    /**
     * Specifies OR ABORT, which will cancel the current INSERT, but all other operations will be preserved in
     * the current transaction.
     *
     * @return this
     */
    public Insert<TModel> orAbort() {
        return or(ConflictAction.ABORT);
    }

    /**
     * Specifies OR FAIL, which does not back out of the previous statements. Anything else in the current
     * transaction will fail.
     *
     * @return this
     */
    public Insert<TModel> orFail() {
        return or(ConflictAction.FAIL);
    }

    /**
     * Specifies OR IGNORE, which ignores any kind of error and proceeds as normal.
     *
     * @return this
     */
    public Insert<TModel> orIgnore() {
        return or(ConflictAction.IGNORE);
    }

    @Override
    public long executeUpdateDelete(DatabaseWrapper databaseWrapper) {
        throw new IllegalStateException("Cannot call executeUpdateDelete() from an Insert");
    }


    public Insert<TModel> orReplace(Pair<IProperty<?>, ?>[] into) {
        return orReplace().columnValues(into);
    }

    public Insert<TModel> orRollback(Pair<IProperty<?>, ?>[] into) {
        return orRollback().columnValues(into);
    }

    public Insert<TModel> orAbort(Pair<IProperty<?>, ?>[] into) {
        return orAbort().columnValues(into);
    }

    public Insert<TModel> orFail(Pair<IProperty<?>, ?>[] into) {
        return orFail().columnValues(into);
    }

    public Insert<TModel> orIgnore(Pair<IProperty<?>, ?>[] into) {
        return orIgnore().columnValues(into);
    }

}
